﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;

namespace yaTDL.Core
{
    public class Recurrence : NotifyObject
    {
        #region Fields

        private int _repeat = 0;
        private string _advancedRepeat = "";
        private bool _isRecurring = false;
        private int _repeatFrom = 0;

        #endregion


        #region Properties

        /// <summary>
        /// An Integer that specifies how the Task repeats. It ranges from 0-8(repeat from original DueDate) and 100-108(repeat from completion-date).
        /// A value of 50 (or 150 respectivey) indicates that the task uses advanced repeating (specified via the AdvancedRepeat property).
        /// </summary>
        public int Repeat
        {
            get { return _repeat; }
            set
            {
                if (value != _repeat)
                {
                    _repeat = value;
                    RaisePropertyChanged("Repeat");
                }
            }
        }

        /// <summary>
        /// A string that specifies how a Task repeats if it uses advanced repeating.
        /// </summary>
        public string AdvancedRepeat
        {
            get { return _advancedRepeat; }
            set
            {
                if (value != _advancedRepeat)
                {
                    _advancedRepeat = value;
                    RaisePropertyChanged("AdvancedRepeat");
                }
            }
        }

        /// <summary>
        /// Indicates whether a Task is repeating.
        /// </summary>
        public bool IsRepeating
        {
            get { return _isRecurring; }
            private set
            {
                if (value != _isRecurring)
                {
                    _isRecurring = value;
                    RaisePropertyChanged("IsRecurring");
                }
            }
        }

        #endregion


        #region Constructor(s)

        /// <summary>
        /// Initializes a new instance of Recurrence.
        /// </summary>
        public Recurrence()
        {
            this.PropertyChanged += new System.ComponentModel.PropertyChangedEventHandler(Recurrence_PropertyChanged);
        }

        #endregion


        #region Public Methods

        /// <summary>
        /// Calculates the next date for a repeating Task.
        /// </summary>
        /// <param name="previousDate">The previous date for the calculation of the new one.</param>
        /// <returns>Returns the next date after previousDate that matches the criteria specified in the Repeat and AdvancedRepeat properties.</returns>
        public DateTime GetNextDate(DateTime previousDate)
        {
            return GetNextDate(previousDate, Repeat, AdvancedRepeat);
        }

        /// <summary>
        /// Calculates the next date for a repeating Task.
        /// </summary>
        /// <param name="previousDate">The previous date for the calculation of the new one.</param>
        /// <param name="repeat">The repeat value to be used for the caculation.</param>
        /// <param name="advancedRepeat">The advanced Repeat value to be used for the calculation (will only be taken into accout if repeat is 50 or 150).</param>
        /// <returns>Returns the next date after previousDate that matches the criteria specified in repeat and advancedRepeat.</returns>
        public static DateTime GetNextDate(DateTime previousDate, int repeat, string advancedRepeat = "")
        {
            if (repeat == 100)
                return previousDate;

            //if the values of repeat are incremented by 100, the Task repeats from the completion Date and not from its original DueDate.
            if (repeat > 100)
                return GetNextDate(DateTime.Now, repeat - 100, advancedRepeat);

            //simple repeat behaviours
            switch (repeat)
            {
                case 0: return previousDate;
                case 1: return previousDate.AddDays(7);
                case 2: return previousDate.AddMonths(1);
                case 3: return previousDate.AddYears(1);
                case 4: return previousDate.AddDays(1);
                case 5: return previousDate.AddDays(14);
                case 6: return previousDate.AddMonths(2);
                case 7: return previousDate.AddMonths(6);
                case 8: return previousDate.AddMonths(3);
            }

            //normalizes and splits up dateString
            advancedRepeat = advancedRepeat.ToLower().Trim();


            string[] elements = advancedRepeat.Split(' ');
            for (int i = 0; i < elements.Length; i++)
            {
                elements[i] += ' ';
                elements[i] = elements[i].Replace("mon ", "monday ");
                elements[i] = elements[i].Replace("tue ", "tuesday ");
                elements[i] = elements[i].Replace("wed ", "wednesday ");
                elements[i] = elements[i].Replace("thu ", "thursday ");
                elements[i] = elements[i].Replace("fri ", "friday ");
                elements[i] = elements[i].Replace("sat ", "saturday ");
                elements[i] = elements[i].Replace("sun ", "sunday ");
                elements[i] = elements[i].Trim();
            }



            //Format 1: "Every X T"
            if (advancedRepeat.StartsWith("every") && elements.Length == 3)
            {
                int factor = Int32.Parse(elements[1]);

                switch (elements[2])
                {
                    case "days":
                        return previousDate.AddDays(factor);
                    case "weeks":
                        return previousDate.AddDays(factor * 7);
                    case "months":
                        return previousDate.AddMonths(factor);
                    case "years":
                        return previousDate.AddYears(factor);
                }
            }


            //Format 2: "On the X D of each month"
            if (advancedRepeat.StartsWith("on the"))
            {
                int x;
                List<int> days_nextMonth = getDaysInMoth(elements[3], previousDate.AddMonths(1).Month, previousDate.AddMonths(1).Year);
                List<int> days_thisMonth = getDaysInMoth(elements[3], previousDate.Month, previousDate.Year);

                if (!Int32.TryParse(elements[2], out x))
                {
                    switch (elements[2])
                    {
                        case "first":
                            x = 0;
                            break;
                        case "second":
                            x = 1;
                            break;
                        case "third":
                            x = 2;
                            break;
                        case "fourth":
                            x = 3;
                            break;
                        case "fifth":
                            x = 4;
                            break;
                        case "last":
                            if (days_thisMonth.Last() > previousDate.Day)
                                x = days_thisMonth.Last();
                            else
                                x = days_nextMonth.Last();
                            break;
                    }
                }
                else
                {
                    x -= 1;
                }


                if (x < days_thisMonth.Count)
                {
                    if (days_thisMonth[x] > previousDate.Day)
                    {
                        return new DateTime(previousDate.Year, previousDate.Month, days_thisMonth[x]);
                    }
                    else
                    {
                        if (x < days_nextMonth.Count)
                        {
                            return new DateTime(previousDate.AddMonths(1).Year, previousDate.AddMonths(1).Month, days_nextMonth[x]);
                        }
                        else
                        {
                            return new DateTime(previousDate.AddMonths(1).Year, previousDate.AddMonths(1).Month, days_nextMonth.Last());
                        }
                    }
                }
            }

            //Format 3: "Every W"
            if (elements.Length == 2 && elements[0] == "every")
            {
                DateTime newDueDate = previousDate;
                if (elements[1] == "weekend")
                {
                    if (!isWeekday(newDueDate.DayOfWeek))
                        newDueDate = newDueDate.AddDays(1);
                    while (isWeekday(newDueDate.DayOfWeek))
                    {
                        newDueDate = newDueDate.AddDays(1);
                    }
                    return newDueDate;
                }
                else if (elements[1] == "weekday")
                {
                    if (isWeekday(newDueDate.DayOfWeek))
                        newDueDate = newDueDate.AddDays(1);
                    while (!isWeekday(newDueDate.DayOfWeek))
                    {
                        newDueDate = newDueDate.AddDays(1);
                    }
                    return newDueDate;
                }
                else
                {
                    DayOfWeek newDay = getDayOfWeek(elements[1]);
                    if (newDueDate.DayOfWeek == newDay)
                    {
                        return newDueDate.AddDays(7);
                    }
                    else
                    {
                        while (newDay != newDueDate.DayOfWeek)
                        {
                            newDueDate = newDueDate.AddDays(1);
                        }
                        return newDueDate;
                    }
                }
            }


            return previousDate;
        }

        #endregion


        #region Private, non-static Methods

        /// <summary>
        /// Updates the IsRecurring Property after Repeat and/or AdvancedRepeat have changed.
        /// </summary>
        private void Recurrence_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
        {
            if (Repeat == 0 || Repeat == 100)
            {
                IsRepeating = false;
            }
            else
            {
                if (Repeat == 50 || Repeat == 150)
                    IsRepeating = String.IsNullOrEmpty(AdvancedRepeat) ? false : true;
                else
                    IsRepeating = true;
            }
        }

        #endregion

        #region Static Methods

        /// <summary>
        /// Returns a List of all Days in the indicated month that match the indicated day.
        /// </summary>
        /// <param name="day">A string with the name of the Day to look for.</param>
        /// <param name="month">The Month that is to be searched.</param>
        /// <param name="year">Specifies the Year.</param>
        /// <returns>Returns a List of all Days in the indicated month that match the indicated day.</returns>
        [DebuggerStepThrough()]
        private static List<int> getDaysInMoth(string day, int month, int year)
        {
            DateTime d = new DateTime(year, month, 1);
            DayOfWeek dayOfWeek;
            List<int> result = new List<int>();

            dayOfWeek = getDayOfWeek(day);

            while (d.Month == month)
            {
                if (d.DayOfWeek == dayOfWeek) result.Add(d.Day);
                d = d.AddDays(1);
            }

            return result;
        }

        /// <summary>
        /// Parses a string and converts it into a DayOfWeek
        /// </summary>
        /// <param name="day">The string to be parsed.</param>
        /// <returns>Returns a DayOfWeek that matches the indicated string.</returns>
        [DebuggerStepThrough()]
        private static DayOfWeek getDayOfWeek(string day)
        {
            DayOfWeek dayOfWeek;
            switch (day.ToLower())
            {
                case "monday": dayOfWeek = DayOfWeek.Monday; break;
                case "tuesday": dayOfWeek = DayOfWeek.Tuesday; break;
                case "wednesday": dayOfWeek = DayOfWeek.Wednesday; break;
                case "thursday": dayOfWeek = DayOfWeek.Thursday; break;
                case "friday": dayOfWeek = DayOfWeek.Friday; break;
                case "saturday": dayOfWeek = DayOfWeek.Saturday; break;
                case "sunday": dayOfWeek = DayOfWeek.Sunday; break;
                default: throw new Exception("Invalid data");
            }

            return dayOfWeek;
        }

        /// <summary>
        /// Checks whether a day is a weekday or not.
        /// </summary>
        /// <param name="day">The day to be checked.</param>
        /// <returns>Returns true for all das from monday to friday and false if 'day' is saturday or sunday.</returns>
        [DebuggerStepThrough()]
        private static bool isWeekday(DayOfWeek day)
        {
            return day == DayOfWeek.Saturday || day == DayOfWeek.Sunday ? false : true;
        }

        #endregion
    }
}


